/*      $Id: mod_java.c 1069 2010-04-26 02:36:16Z phrakt $    */
/*
 * Copyright (c) 2007 Jean-Francois Brousseau <phrakt@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/types.h>

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

#include <canopy/log.h>
#include <canopy/mem.h>
#include <canopy/conf.h>

#include <httpd/httpd.h>

#include "mod_java.h"



/*
 * Converts a jlong into a void pointer
 */
#define JLONG_TO_PTR(l)		((void *)(uintptr_t)l)

/*
 * Converts a void pointer into a jlong
 */
#define PTR_TO_JLONG(p)		((jlong)(uintptr_t)p)




struct mod_java_hdlr {
	jobject		hdlr_obj;	/* reference to the handler object */
	jmethodID       hdlr_method;	/* reference to the handle() method */
};


/*
 * This table maps the JNI return codes to error strings.  Because the JNI
 * return codes are returned as negative values, it is necessary to flip the
 * sign on the return value before using it as an index.
 *
 */
static const char *mod_java_jnierrs[] = {
	"OK",
	"Unknown error",
	"Thread detached to JVM",
	"JNI version mimatch",
	"Not enough memory",
	"VM already created",
	"Invalid argument",
};



static JavaVM *mod_java_jvm = NULL;


#if defined(JNI_VERSION_1_2)

#define MOD_JAVA_JNI_VERSION	JNI_VERSION_1_2

static JavaVMInitArgs  mod_java_vmargs;
static JavaVMOption    mod_java_vmopts[2] = {
	{ "-Djava.class.path=/usr/local/lib/canopy:/usr/local/lib/canopy/mod_java-api-0.1.jar", NULL },
	{ "-Djava.home=/usr/local/jdk1.5.0", NULL },
};

#elif defined(JNI_VERSION_1_1)

#define MOD_JAVA_JNI_VERSION	JNI_VERSION_1_1

static JDK1_1InitArgs  mod_java_vmargs;

#endif


static jstring platformEncoding = NULL;


/*
 * Global refererences to the classes that we use to wrap around the
 * http_sock_t and http_msg_t structures for handler invocation.
 */
static jclass mod_java_socket_class;
static jclass mod_java_request_class;
static jclass mod_java_response_class;

/* reference to the class that we use to build Java String objects */
static jclass      mod_java_string_class;
static jmethodID   mod_java_string_cstr;



extern cnp_conf_t *httpd_conf;

static int  mod_java_config_handler   (int, char **);
static int  mod_java_handler_wrapper  (struct httpd_hdlr_args *);


/*
 * mod_java_init()
 *
 * Initialize the Java Virtual Machine and preload classes that will be used
 * at runtime.
 *
 * Returns 0 on success, or -1 on failure.
 */
int
mod_java_init(void)
{
	int ret = 0;

	if (cnp_conf_setdir(httpd_conf, MOD_JAVA_CONF_DIR,
	    mod_java_config_handler, 2, 3) == -1) {
		canopy_log(CNP_LOG_ERR, "failed to register directive");
		return (-1);
	}

	if (mod_java_jvminit() == -1)
		return (-1);

	return (ret);
}

/*
 * mod_java_cleanup()
 *
 * Shutdown the Java Virtual Machine and free all resources.
 */
void
mod_java_cleanup(void)
{
	mod_java_jvmstop();
}

/*
 * mod_java_config_handler()
 *
 * Handler for the configuration directive that loads a Java class and
 * registers an HTTP handler with an instance of that class.
 */
static int
mod_java_config_handler(int argc, char **argv)
{
	jclass class;
	jobject obj;
	jmethodID method;
	JNIEnv *env;
	char hdlr_name[32];
	enum httpd_phase phase;
	struct mod_java_hdlr *hdlr;

	if ((class = mod_java_getclass(env, argv[1], 0)) == NULL) {
		return (-1);
	}

	if ((obj = (*env)->AllocObject(env, class)) == NULL) {
		(*env)->DeleteLocalRef(env, class);
		return (-1);
	}

	/* we don't need the class anymore, release our reference */
	(*env)->DeleteLocalRef(env, class);

	method = (*env)->GetMethodID(env, class, "handle",
	    "(Lca/terraquati/canopy/http/HTTPSocket;"
	    "Lca/terraquati/canopy/http/HTTPRequest;"
	    "Lca/terraquati/canopy/http/HTTPResponse;)I");
	if (method == NULL) {
		(*env)->DeleteLocalRef(env, obj);
		return (-1);
	}

	if ((hdlr = cnp_malloc(sizeof(*hdlr))) == NULL) {
		(*env)->DeleteLocalRef(env, obj);
		return (-1);
	}

	hdlr->hdlr_obj = (*env)->NewGlobalRef(env, obj);
	hdlr->hdlr_method = method;

	(*env)->DeleteLocalRef(env, obj);

	canopy_addhdlr(phase, HTTPD_HDLR_TAIL, hdlr_name,
	    mod_java_handler_wrapper, hdlr);

	return (0);
}

/*
 * mod_java_jvminit()
 *
 * Start the Java Virtual Machine.
 */
int
mod_java_jvminit(void)
{
	jint ret;
	jclass local;
	JNIEnv *jni_env;

	cnp_log(httpd_logchan, CNP_LOG_DEBUG, "using JNI version %u.%u",
	    MOD_JAVA_JNI_VERSION >> 16, MOD_JAVA_JNI_VERSION & 0xf);

	memset(&mod_java_vmargs, 0, sizeof(mod_java_vmargs));
	mod_java_vmargs.version = MOD_JAVA_JNI_VERSION;
	mod_java_vmargs.options = mod_java_vmopts;
	mod_java_vmargs.nOptions = 2;

	ret = JNI_GetDefaultJavaVMInitArgs(&mod_java_vmargs);
	if (ret != JNI_OK) {
		canopy_log(CNP_LOG_ERR, "failed to initialize JVM arguments");
		return (-1);
	}

	cnp_log(httpd_logchan, CNP_LOG_DEBUG,
	    "creating new Java Virtual Machine");
	ret = JNI_CreateJavaVM(&mod_java_jvm, (void **)&jni_env,
	    &mod_java_vmargs);
	if (ret != JNI_OK) {
		canopy_log(CNP_LOG_ERR, "failed to create Java VM");
		return (-1);
	}

	/*
	 * Get global references to the classes that we will be using
	 * to run handlers.
	 */
	mod_java_string_class = mod_java_getclass(jni_env,
	    "java.lang.String", 1);

	mod_java_socket_class = mod_java_getclass(jni_env,
	    MOD_JAVA_SOCKET_CLASS, 1);

	mod_java_request_class = mod_java_getclass(jni_env,
	    MOD_JAVA_REQUEST_CLASS, 1);

	mod_java_response_class = mod_java_getclass(jni_env,
	    MOD_JAVA_RESPONSE_CLASS, 1);

	if ((mod_java_load_Canopy(jni_env) == -1) ||
	    (mod_java_load_HTTPMessage(jni_env) == -1) ||
	    (mod_java_load_HTTPSocketImpl(jni_env) == -1))
		return (-1);


	return (0);
}

/*
 * mod_java_jvmstop()
 *
 * Delete all global references from the JVM and destroy it.
 *
 * Returns 0 on success, or -1 on failure.
 */
int
mod_java_jvmstop(void)
{
	jint ret;
	JNIEnv *jni_env;

	canopy_log(CNP_LOG_DEBUG, "attaching current thread to JVM");
	ret = (*mod_java_jvm)->AttachCurrentThread(mod_java_jvm,
	    (void **)&jni_env, NULL);
	if (ret != JNI_OK) {
		canopy_log(CNP_LOG_ERR, "failed to attach thread to JVM");
	}
	else {
		(*jni_env)->DeleteGlobalRef(jni_env, mod_java_socket_class);
		(*jni_env)->DeleteGlobalRef(jni_env, mod_java_request_class);
		(*jni_env)->DeleteGlobalRef(jni_env, mod_java_response_class);
	}

	ret = (*mod_java_jvm)->DestroyJavaVM(mod_java_jvm);
	if (ret != JNI_OK) {
		return (-1);
	}

	canopy_log(CNP_LOG_INFO, "JVM stopped");
	mod_java_jvm = NULL;

	return (0);
}

/*
 * mod_java_getclass()
 *
 * Loads a Java class whose name is specified in <name> through the JNI
 * environment and return a reference to that class.  The type of reference
 * that is returned depends on the <global> argument: a value of 0 indicates
 * that a local reference should be returned, and a non-0 value indicates that
 * the reference should be made global.
 *
 * The class' name must be fully-qualified (including the entire package name).
 *
 * Returns a pointer to the Java class handle on success, or NULL on failure.
 */
jclass
mod_java_getclass(JNIEnv *env, const char *name, int global)
{
	char pathbuf[256], *np;
	jclass local, cls;

	canopy_log(CNP_LOG_DEBUG, "loading class %s", name);

	/* transform the class name into a file path */
	strlcpy(pathbuf, name, sizeof(pathbuf));
	for (np = pathbuf; *np != '\0'; np++)
		if (*np == '.')
			*np = '/';

	canopy_log(CNP_LOG_DEBUG, "class %s mapped to path %s", name, pathbuf);

	if ((cls = (*env)->FindClass(env, pathbuf)) == NULL) {
		canopy_log(CNP_LOG_ERR, "failed to find class %s", name);
		return (NULL);
	}

	if (global) {
		local = cls;
		cls = (*env)->NewGlobalRef(env, local);
		(*env)->DeleteLocalRef(env, local);
	}

	return (cls);
}

/*
 * mod_java_handler_wrapper()
 *
 * This Canopy handler acts as the glue between the Canopy core and the Java
 * handlers.  It creates Java objects and maps them to the <sock>, <req>,
 * and <resp> arguments, and calls the Java handler's method with those
 * objects as arguments.
 *
 */
static int
mod_java_handler_wrapper(struct httpd_hdlr_args *args)
{
	int code;
	jint ret;
	jobject jsock, jreq, jresp;
	jthrowable t;
	JNIEnv *jni_env;
	struct mod_java_hdlr *hdlr;

	/*
	 * First, attach this thread to the JVM
	 */
	canopy_log(CNP_LOG_DEBUG, "attaching current thread to JVM");
	ret = (*mod_java_jvm)->AttachCurrentThread(mod_java_jvm,
	    (void **)&jni_env, NULL);
	if (ret != JNI_OK) {
		canopy_log(CNP_LOG_ERR, "failed to attach thread to JVM");
		return (HTTPD_HDLR_ERR);
	}

	/*
	 * Allocate the Java objects that will be bound to the <sock>, <req>
	 * and <resp> arguments passed to this handler.
	 */
	jsock = (*jni_env)->AllocObject(jni_env, mod_java_socket_class);
	if (jsock == NULL) {
		canopy_log(CNP_LOG_ERR, "failed to allocate Java socket object");
	}

	jreq = (*jni_env)->AllocObject(jni_env, mod_java_request_class);
	if (jreq == NULL) {
		canopy_log(CNP_LOG_ERR, "failed to allocate Java request object");
	}

	jresp = (*jni_env)->AllocObject(jni_env, mod_java_response_class);
	if (jreq == NULL) {
		canopy_log(CNP_LOG_ERR, "failed to allocate Java response object");
	}

	canopy_log(CNP_LOG_DEBUG, "calling handle() method");
	ret = (*jni_env)->CallIntMethod(jni_env, hdlr->hdlr_obj,
	    hdlr->hdlr_method, jsock, jreq, jresp);

	if ((t = (*jni_env)->ExceptionOccurred(jni_env)) != NULL) {
		(*jni_env)->ExceptionClear(jni_env);
	}

	(*jni_env)->DeleteLocalRef(jni_env, jsock);
	(*jni_env)->DeleteLocalRef(jni_env, jreq);
	(*jni_env)->DeleteLocalRef(jni_env, jresp);

	canopy_log(CNP_LOG_DEBUG, "detaching current thread from JVM");
	ret = (*mod_java_jvm)->DetachCurrentThread(mod_java_jvm);
	if (ret != JNI_OK) {
	}

	return (HTTPD_HDLR_OK);
}

/*
 * mod_java_string_ctoj()
 *
 * Create a Java String object from the C string specified in <buf>.
 *
 * Returns a local reference to the String object on success, or NULL on
 * failure.
 */
jstring
mod_java_string_ctoj(JNIEnv *env, const char *buf)
{
	size_t len;
	jstring string;
	jbyteArray bytes;

	len = strlen(buf);

	if ((*env)->EnsureLocalCapacity(env, 2) != JNI_OK) {
		return (NULL);
	}

	if ((bytes = (*env)->NewByteArray(env, len)) == NULL) {
		return (NULL);
	}

	(*env)->SetByteArrayRegion(env, bytes, 0, len, (jbyte *)buf);
	string = (*env)->NewObject(env, mod_java_string_class,
	    mod_java_string_cstr, bytes);

	(*env)->DeleteLocalRef(env, bytes);

	return (string);
}

/*
 * mod_java_string_jtoc()
 *
 * Create a Java String object from the C string specified in <buf>.
 *
 * Returns a local reference to the String object on success, or NULL on
 * failure.
 */
ssize_t
mod_java_string_jtoc(JNIEnv *env, jstring jstr, char *dst, size_t len)
{
	jboolean copy;
	const char *str;

	str = (*env)->GetStringUTFChars(env, jstr, &copy);

	return (0);
}

/*
 * mod_java_getpeer()
 *
 * Get the value of the peer object field whose name is <field> from the object
 * <obj> and cast the value into a memory address.  The peer field must be
 * a Java long variable.
 *
 */
void*
mod_java_getpeer(JNIEnv *env, jobject obj, const char *field)
{
	jclass class;
	jlong value;
	jfieldID fid;
	void *ptr;

	class = (*env)->GetObjectClass(env, obj);
	if (class == NULL) {
		return (NULL);
	}

	fid = (*env)->GetFieldID(env, class, field, "J");
	if (fid == NULL) {
		(*env)->DeleteLocalRef(env, class);
		return (NULL);
	}

	value = (*env)->GetLongField(env, obj, fid);

	ptr = JLONG_TO_PTR(value);

	return (ptr);
}

/*
 * mod_java_setpeer()
 *
 * Set the value associated with a peer object field whose name is <field> on
 * the object <obj> to the value of <addr>.  The peer field must be a Java long
 * variable in order to properly store addresses on 64-bit platforms.
 *
 * Returns 0 on success, or -1 on failure.
 */
int
mod_java_setpeer(JNIEnv *env, jobject obj, const char *field, const void *addr)
{
	jclass class;
	jfieldID fid;
	jlong value = PTR_TO_JLONG(addr);

	class = (*env)->GetObjectClass(env, obj);
	if (class == NULL) {
		return (-1);
	}

	fid = (*env)->GetFieldID(env, class, field, "J");
	if (fid == NULL) {
		(*env)->DeleteLocalRef(env, class);
		return (-1);
	}

	(*env)->SetLongField(env, obj, fid, value);

	return (0);
}

/*
 * mod_java_jnierr()
 *
 * Returns a JNI error string from a JNI return value.
 */
const char*
mod_java_jnierr(jint ret)
{
	/* value makes no sense for this JNI version */
	if ((ret < JNI_EINVAL) || (ret > 0))
		return (NULL);

	return mod_java_jnierrs[-ret];
}
